package niu.toy.spring.mvc.impl;

import cn.hutool.core.util.ReflectUtil;

import niu.toy.spring.ioc.annotation.Nullable;
import niu.toy.spring.support.ParameterNameDiscoverer;
import niu.toy.spring.mvc.HandlerMethod;
import niu.toy.spring.mvc.HandlerMethodParameter;
import niu.toy.spring.mvc.ModelAndViewContainer;
import niu.toy.spring.mvc.NativeWebRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.InvocationTargetException;

public class HandlerWrapper {
    private static final Logger logger = LoggerFactory.getLogger(HandlerWrapper.class);



    private ArgumentResolverComposite argumentResolvers;
    private ParameterNameDiscoverer parameterNameDiscoverer;
    private HandlerMethod wrappedHandler;

    public HandlerWrapper(HandlerMethod handlerMethod) {
        this.wrappedHandler=handlerMethod;
    }
    public void setArgumentResolvers(ArgumentResolverComposite argumentResolvers) {
        this.argumentResolvers = argumentResolvers;
    }

    public void setParameterNameDiscoverer(ParameterNameDiscoverer parameterNameDiscoverer) {
        this.parameterNameDiscoverer = parameterNameDiscoverer;
    }

    public  Object invokeHandler(NativeWebRequest request, ModelAndViewContainer mavContainer,
                                 Object... providedArgs) throws Exception {

        Object[] args = getMethodArgumentValues(request, mavContainer,providedArgs);
        Object returnValue = doInvoke(args);
        return returnValue;
    }
    private Object[] getMethodArgumentValues(NativeWebRequest request,ModelAndViewContainer mavContainer,
                                             Object... providedArgs) throws Exception {

        HandlerMethodParameter[] parameters = wrappedHandler.getMethodParameters();
        Object[] args = new Object[parameters.length];
        for (int i = 0; i < parameters.length; i++) {
            HandlerMethodParameter parameter = parameters[i];
            parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
            args[i] = resolveProvidedArgument(parameter, providedArgs);
            if (args[i] != null) {
                continue;
            }
            if (this.argumentResolvers.supportsParameter(parameter)) {
                try {
                    args[i] = this.argumentResolvers.resolveArgument(parameter, request,mavContainer);
                    continue;
                }
                catch (Exception ex) {
                    if (logger.isDebugEnabled()) {
                        logger.debug(getArgumentResolutionErrorMessage("Failed to resolve", i), ex);
                    }
                    throw ex;
                }
            }
            if (args[i] == null) {
                throw new IllegalStateException("Could not resolve method parameter at index " +
                        parameter.getParameterIndex() + " in " + parameter.toString() +
                        ": " + getArgumentResolutionErrorMessage("No suitable resolver for", i));
            }
        }
        return args;
    }
    @Nullable
    private Object resolveProvidedArgument(HandlerMethodParameter parameter, @Nullable Object... providedArgs) {
        if (providedArgs == null) {
            return null;
        }
        for (Object providedArg : providedArgs) {
            if (parameter.getParameterType().isInstance(providedArg)) {
                return providedArg;
            }
        }
        return null;
    }

    /**
     * 通过反射调用 Action 方法
     * @param args
     * @return
     * @throws Exception
     */
    protected Object doInvoke(Object... args) throws Exception {
        //取消类型安全检测（可提高反射性能）
        ReflectUtil.setAccessible(this.wrappedHandler.getMethod());
        try {
            return this.wrappedHandler.getMethod().invoke(this.wrappedHandler.getBean(), args);
        }
        catch (IllegalArgumentException ex) {
            String text = (ex.getMessage() != null ? ex.getMessage() : "Illegal argument");
            throw new IllegalStateException(getInvocationErrorMessage(text, args), ex);
        }
        catch (InvocationTargetException ex) {
            // Unwrap for HandlerExceptionResolvers ...
            Throwable targetException = ex.getTargetException();
            if (targetException instanceof RuntimeException) {
                throw (RuntimeException) targetException;
            }
            else if (targetException instanceof Error) {
                throw (Error) targetException;
            }
            else if (targetException instanceof Exception) {
                throw (Exception) targetException;
            }
            else {
                String text = getInvocationErrorMessage("Failed to invoke handler method", args);
                throw new IllegalStateException(text, targetException);
            }
        }
    }
    private String getInvocationErrorMessage(String text, Object[] resolvedArgs) {
        StringBuilder sb = new StringBuilder(getDetailedErrorMessage(text));
        sb.append("Resolved arguments: \n");
        for (int i = 0; i < resolvedArgs.length; i++) {
            sb.append("[").append(i).append("] ");
            if (resolvedArgs[i] == null) {
                sb.append("[null] \n");
            }
            else {
                sb.append("[type=").append(resolvedArgs[i].getClass().getName()).append("] ");
                sb.append("[value=").append(resolvedArgs[i]).append("]\n");
            }
        }
        return sb.toString();
    }
    private String getArgumentResolutionErrorMessage(String text, int index) {
        Class<?> paramType = this.wrappedHandler.getMethodParameters()[index].getParameterType();
        return text + " argument " + index + " of type '" + paramType.getName() + "'";
    }
    protected String getDetailedErrorMessage(String text) {
        StringBuilder sb = new StringBuilder(text).append("\n");
        sb.append("HandlerMethod details: \n");
        sb.append("Controller [").append(this.wrappedHandler.getBeanType().getName()).append("]\n");
        sb.append("Method [").append(this.wrappedHandler.getMethod().toGenericString()).append("]\n");
        return sb.toString();
    }
}
